import { EventTarget } from "cc";
import merge from "./lodash.merge.js";
/**
 * @property `url` WebSocket服务端地址
 * @property `publicKey` WebSocket所需的PublicKey
 * @property `timeout` 连接超时时间，单位ms，默认3000ms
 * @property `heartBeatTimeout` 心跳超时时间，单位ms，默认45000ms
 * @property `reconnect` WebSocket重连配置
 * @property `reconnect.retries` 重连尝试次数，默认尝试10次
 * @property `reconnect.min` 重连时最小间隔时间，默认500ms
 * @property `reconnect.max` 重连时最大间隔时间，默认Infinity
 *
 * 重连时的间隔时间为500ms -> 1000ms -> 2000ms -> 4000ms -> 最大间隔时间
 */
export type WebSocketConfigType = {
    /**
     * WebSocket服务端地址
     */
    url: string;
    /**
     * WebSocket所需的token
     */
    token: string;
    /**
     * 连接超时时间，单位ms
     */
    timeout?: number;
    /**
     * 心跳超时时间，单位ms，默认45000ms
     */
    heartBeatTimeout?: number;
    /**
     * WebSocket重连配置
     */
    reconnect?: {
        /**
         * 重连尝试次数，默认尝试10次
         */
        retries?: number;
        /**
         * 重连时最小间隔时间，默认500ms
         */
        min?: number;
        /**
         * 重连时最大间隔时间，默认Infinity
         */
        max?: number;
    };
};

export type WebSocketMessageHandler = (data: any, cmd: string, code: number) => void;

export enum WebSocketStatus {
    "CONNECTING",
    "OPEN",
    "CLOSING",
    "CLOSED",
}

/**
 * WebSocket消息类型
 */
enum MessageType {
    /**
     * 正常消息
     */
    "MESSAGE" = "0",
    /**
     * 第一次链接到服务器
     */
    "CONNECTED" = "1",
    /**
     * 心跳ping
     */
    "PING" = "2",
    /**
     * 心跳pong
     */
    "PONG" = "3",
}

const DefaultConfig = {
    timeout: 3000,
    heartBeatTimeout: 45000,
    reconnect: {
        min: 500,
        max: Infinity,
        retries: 10,
    },
} as WebSocketConfigType;

export class WebSocketController extends EventTarget {
    private _jwt: string | undefined;
    private _token = "";
    private _isConnected = false;
    private _ws: WebSocket | null = null;
    private _url = "";
    private _lostHeartCount = 0;
    private _waitingPong = false;
    private _config: WebSocketConfigType | null = null;

    get status(): WebSocketStatus {
        return (this._ws?.readyState as WebSocketStatus) || WebSocketStatus.CONNECTING;
    }
    private _onMessage({ data: message }: any) {
        this._isConnected = true;

        const type = message.substring(0, 1);
        switch (type) {
            case MessageType.MESSAGE:
                try {
                    const { cmd, data } = JSON.parse(message.substring(1));

                    this.emit(cmd, data);
                } catch (error) {
                    console.error("WebSocket received message[error].", error, message);
                }
                break;
            case MessageType.CONNECTED:
                //开始心跳检测
                this._startHeartBeat();
                this.emit("ws::connected");
                break;
            case MessageType.PONG:
                this._waitingPong = false;
                //收到心跳信息
                break;
        }
    }
    private _connect() {
        return new Promise<WebSocketController>((resolve, reject) => {
            if (!this._config) return;

            const { url, token } = this._config;
            this._url = `${url}?token=${token}`;
            this._token = token;
            const timeoutTimer = setTimeout(() => reject("timeout"), this._config.timeout);

            const ws = (this._ws = new WebSocket(this._url));
            ws.onopen = (e) => {
                console.log("Websocket onOpen.", e);
                clearTimeout(timeoutTimer);
                resolve(this);
                this._isConnected = true;
                setTimeout(() => this.emit("ws::open", this), 0);
            };
            ws.onclose = ({ code }) => {
                console.log("Websocket onClose.", code);
                if (this._isConnected) {
                    this._isConnected = false;
                    this.emit("ws.close", code);
                } else {
                    ws.onopen = null;
                    ws.onmessage = null;
                    clearTimeout(timeoutTimer);
                    reject(code);
                }
            };
            // ws.onerror = (e) => {
            //     console.log("Websocket onError.", e);
            // };
            ws.onmessage = this._onMessage.bind(this);
        });
    }
    private async _reconnect(delay: number, retries: number) {
        console.log("WebSocket _reconnect", delay, retries);
        try {
            await this._connect();
        } catch (error) {
            retries++;
            if (retries > (this._config as any).reconnect.retries) {
                return false;
            }
            if (delay > (this._config as any).reconnect.max) {
                delay = (this._config as any).reconnect.max;
            }
            console.log("WebSocket _reconnect reconnect", delay, retries);
            setTimeout(() => {
                this._reconnect(delay * 2, retries);
            }, delay);
        }
    }
    private _startHeartBeat() {
        if (!this._isConnected) return;

        if (this._waitingPong) {
            this._lostHeartCount++;
        }
        if (this._lostHeartCount >= (this._config as any).heartBeatTimeout / 1000) {
            console.log("WebSocket heartbeat timeout.");
            this.close();
            if ((this._config as any).reconnect.retries > 0) {
                this._reconnect((this._config as any).reconnect.min, 0);
            }
            return;
        }

        this._ws?.send(MessageType.PING);
        this._waitingPong = true;

        setTimeout(() => this._startHeartBeat(), 1000);
    }
    async connect(config: WebSocketConfigType): Promise<WebSocketController> {
        this._config = merge({}, DefaultConfig, config) as WebSocketConfigType;
        try {
            return await this._connect();
        } catch (error) {
            this._reconnect((this._config as any).reconnect.min, 0);
            return Promise.resolve(this);
        }
    }

    send(cmd: string, data?: Record<string, any>) {
        if (!this._token || !this._ws) {
            console.error("WebSocket is not initialized.");
            return this;
        }

        const _params = JSON.stringify({ cmd, token: this._token, data });

        this._ws.send(`${MessageType.MESSAGE}${_params}`);
        return this;
    }
    close(code?: number) {
        this._ws?.close(code);
        this._ws = null;
        this._lostHeartCount = 0;
        // this._isConnected = false;
        this._waitingPong = false;
        return this;
    }
}
export default new WebSocketController();
